package bloodfang;

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;

import bloodfang.data.KeyValueStore;
import bloodfang.data.RandomPropPoints;

public abstract class Cycle {
	
	protected static final double PRECISION = Math.pow(10, -8);
	private static RandomPropPoints rpp = new RandomPropPoints();
	
	protected Stats stats;
	protected final KeyValueStore aps = new KeyValueStore();
	protected final KeyValueStore dps = new KeyValueStore();
	private HashMap<ProcInfo, Double> procUptimes = new HashMap<ProcInfo, Double>();
	
	public abstract double get_dps();
	
	public void show_aps_breakdown() {
		System.out.println("APS Breakdown:");
		aps.print();
	}
	
	public void show_dps_breakdown() {
		for(String attack : dps.keySet()){
			String perc = String.format("(%2.2f%%)", (double) (dps.get(attack)/dps.sum()*100));
			double dps1 = dps.get(attack);
			System.out.format("%-25s%18.2f%10s%n", attack, dps1, perc);
		}
	}
	
	public void show_proc_uptimes() {
		for(ProcInfo proc : procUptimes.keySet()){
			System.out.format("%-36s%8.5f%n", proc, procUptimes.get(proc));
		}
	}
	
	protected final double get_procs_per_second(ProcData gbd, double mh_hits_per_sec, double oh_hits_per_sec, double other_hits_per_sec){
		if(gbd.behavior.isRPPM())
			return proc_rate(gbd, 1.8);
		double valid_hits_per_sec = mh_hits_per_sec + oh_hits_per_sec + other_hits_per_sec;
		double proc_rate = proc_rate(gbd, 1.8);
//		if(gbd.mh_only()) {
//			valid_hits_per_sec = mh_hits_per_sec;
//			proc_rate = proc_rate(gbd, stats.getMh().getSpeed());
//		}else if(gbd.oh_only()) {
//			valid_hits_per_sec = oh_hits_per_sec;
//			proc_rate = proc_rate(gbd, stats.getOh().getSpeed());
//		}
//		if(gbd.onlyCrit()) {
//			valid_hits_per_sec *= stats.getCritPercentStatic();
//		}
		return valid_hits_per_sec * proc_rate;
	}
	
	private final double proc_rate(ProcData gbd, double speed){
        if(gbd.behavior.isPPM()) {
        	return gbd.behavior.ppm * speed / 60.;
        } else if (gbd.behavior.isRPPM()) {
        	double hasted_speed = 1.;
        	if(gbd.behavior.isScalingWithHaste())
        		hasted_speed = get_hasted_speed();
        	return hasted_speed * gbd.behavior.rppm / 60;
        } else{
        	return gbd.behavior.proc_chance;
        }
	}
	
	protected double get_hasted_speed(){
		return get_hasted_speed(stats.getBuffed(Stat.hst));
	}
	
	protected double get_hasted_speed(double haste){
		double speed = 1 * (1 + 0.01 * haste / RATING.HASTE_CONVERSION_RATING);
		if(stats.getRace() == Race.troll)
			speed *= 1 + .2 * 10. / 180;
		if(stats.hasRaidBuff(RaidBuff.haste_buff))
			speed *= 1.05;
		speed *= (1 + 0.3 * 40 / stats.getSettings().FIGHT_LENGTH);
		return speed;
	}
	
	private double get_lambda(ProcData gbd, double haste){
		double hasted_speed = 1.;
		if(gbd.behavior.isScalingWithHaste())
			hasted_speed = get_hasted_speed(haste);
		return hasted_speed * gbd.behavior.rppm / 60 * gbd.behavior.duration;
	}
	
	// TODO avoid static functions
	protected static double get_scaling_value(ProcData procData, int upgrade_level){
		int steps = procData.blue ? 8 : 4;
		int itemlevel = procData.base_lvl + steps * upgrade_level;
		int scaling = rpp.get(itemlevel,procData.blue);
		return scaling;
	}
	
	// TODO avoid static functions
	protected static double get_scaling_value_by_item_level(ProcData procData, int itemlevel){
		int scaling = rpp.get(itemlevel,procData.blue);
		return scaling;
	}
	
	protected final void procs(double total_mh_hits, double total_oh_hits, double total_other_hits){
		stats.setProcStats(new StatBlock()); // clear procStats object
		for(ProcInfo procInfo : stats.getProcs()){
			ProcData procData = stats.getProcData().get(procInfo.getProcName());
			if(procData == null){
				System.err.println("Error: " + procInfo + " not in procData DB");
				continue;
			}
			ProcBehavior behavior = procData.behavior;
			if(behavior.isType(ProcType.BUFF)){
				double procs_per_second = get_procs_per_second(procData, total_mh_hits, total_oh_hits, total_other_hits);
				double uptime = 1.0;
				if(behavior.hasICD()){
					if(behavior.isRPPM()){
						double hasted_speed = 1.;
						if(behavior.isScalingWithHaste())
							hasted_speed = get_hasted_speed();
						double time_to_proc = 60/(hasted_speed * behavior.rppm) + procData.behavior.icd - Math.min(procData.behavior.icd, 10);
						uptime = 1.1307 * behavior.duration / time_to_proc;
					}else{
						uptime *= behavior.duration / (behavior.icd + 1 / procs_per_second);
					}
				}
				if(!behavior.hasICD() && (behavior.isPPM() || behavior.isRPPM())){
					if(behavior.isRPPM()){
						if(behavior.hasStacks()){
							double lambda = get_lambda(procData, this.stats.getBuffed(Stat.hst));
							double e_lambda = Math.exp(lambda);
							double e_minus_lambda = Math.exp(-lambda);
							uptime = 1.1307 * (e_lambda-1)*(1 - Math.pow((1-e_minus_lambda),procData.behavior.stacks));
						}else{
							double lambda = get_lambda(procData, stats.getBuffed(Stat.hst));
							uptime = 1.1307 * (1 - Math.exp(-lambda));
						}
					}else{
						if(behavior.hasStacks()){
							double q = 1 - procs_per_second;
			                double Q = Math.pow(q , behavior.duration);
			                double P = 1 - Q;
			                uptime = P * (1 - Math.pow(P, behavior.stacks)) / Q;
						} else{
							uptime *= 1 - Math.pow(1 - procs_per_second, behavior.duration);
						}
					}
				}
				else if(behavior.hasStacks()){
					uptime *= behavior.stacks;
				}
//				System.out.println(procInfo + " uptime " + uptime);
				procUptimes.put(procInfo,uptime);
				if(behavior.scaling){
					int steps = procData.blue ? 8 : 4;
					int itemlevel = procData.base_lvl + steps * procInfo.getUpgradeLevel();
					int scaling = rpp.get(itemlevel,procData.blue);
//					System.out.println(procInfo);
					stats.getProcStats().add(behavior.stats,uptime, scaling);
				}else{
					// TODO Rune of Re-Origination
					stats.getProcStats().add(behavior.stats,uptime);
				}
			}
			else if(behavior.isType(ProcType.DIRECT_DMG)){
				if(procData.name.equals("capacitive_primal_diamond")){
					// modify rppm by spec
					behavior.rppm = stats.getProcData().getCapacitivePrimalDiamondRPPM(stats.getSettings().spec);
				}
				double procs_per_second = get_procs_per_second(procData, total_mh_hits, total_oh_hits, total_other_hits);
				if(procData.name.endsWith("elemental_force")){
					aps.put(procData.name, procs_per_second);
				}
				else {
					double time_to_proc = (behavior.icd + 1 / procs_per_second);
					if(procData.behavior.isRPPM())
						//time_to_proc = (2 - (5 / (Math.exp(3./2)))) / procs_per_second;
						time_to_proc = 1 / procs_per_second;
					//System.out.println(gbd.name + " ttp " + time_to_proc);
					double aps3 = 1.0 / time_to_proc;
					if(behavior.hasStacks()){
						// we are only having the case that something makes dmg when at
						// #stacks, so this is correct, if we get another mechanic we need
						// to change this
						aps3 /= behavior.stacks;
					}
					aps.put(procInfo.getProcName(), aps3);
				}
			}
		}
		if(stats.getSettings().use_cache)
			this.stats.reCache();
	}
	
	public static final Cycle createCycle(Spec spec,Stats stats,Attacks attacks){
		Class<? extends Cycle> clazz = spec.getCycle();
		try {
			return clazz.getConstructor(Stats.class,Attacks.class).newInstance(stats, attacks);
		} catch (SecurityException | NoSuchMethodException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
			e.printStackTrace();
		}
		return null;
	}
}
